iT邦幫忙

2022 iThome 鐵人賽

DAY 18
0
自我挑戰組

嘗試30天學「不」會Rust系列 第 18

[Rust] Function programming in Rust - 閉包

  • 分享至 

  • xImage
  •  

環境

OS: Windows 10
Editor: Visual Studio Code
Rust version: 1.63.0

建立閉包

閉包,如今在許多程式語言都有實作這項功能,可以把函式實作的部分(匿名函式)賦職給其他變數,或是當成參數傳入其他函式內。

或許會說,C的函式指標(function pointer)不就可以做到這些事嗎?是沒錯,但在作用域(scope)的規範下,閉包能做到的還是跟函式指標有區別的,下面則會說到這部分,先來看看如何在Rust中使用閉包。

首先,這是一個簡單的函式:

fn add_one(x: u32) -> u32 {
    x + 1
}

然後以下是,把上面的函式變為閉包的樣子,可以看到有不同的寫法,可以發現定義閉包,都是以||開始,裡面放的都會是要傳入參數:

fn main() {
    let add_one_v1 = |x: u32| -> u32 { x + 1 };
    let add_one_v2 = |x| { x + 1 };
    let add_one_v3 = |x| x + 1;
}

如果把上面的方法複製拿來用的話,會發生錯誤,因為compiler不知道參數跟回傳值是什麼型別。add_one_v2add_one_v3會需要在使用後,由compiler推導他的型別才會生效。

要使用閉包的話,跟使用函式的方式一樣:

add_one_v1(3);
add_one_v2(4);
add_one_v3(5);

PS: add_one_v2可能會被rust analyzer簡化成add_one_v3的樣子,因為實作只有入參數加1,然後回傳忠家沒有任何陳述句(statement)。

如果沒有要傳入任何參數的話,會是長這樣,以我們要一個回傳2的函式為例:

let two = || 2; // `two`的type是`|| -> i32`
println!("{}!", two());

再來請注意這樣的閉包定義方式,使用的時候,會是由compiler推導使用型別,假設連續呼叫著閉包,但傳入的都是不同型別,compiler會丟出錯誤,因為在第一次使用的時候,這個閉包已經被判斷為第一次使用的型別了:

let return_val = |x| x;
let s = return_val(String::from("Example"));
let p = return_val(2); //ERROR!
Compiling basic v0.1.0 (/Users/liangcharlie/Workspace/rust-learning/basic)
error[E0308]: mismatched types
  --> src/main.rs:20:24
   |
20 |     let p = return_val(2);
   |                        ^- help: try using a conversion method: `.to_string()`
   |                        |
   |                        expected struct `String`, found integer

For more information about this error, try `rustc --explain E0308`.

coompiler會說閉包使用的時候,推倒的時候已經是String型別,但第二次呼叫的時候是i32。這是不允許的。

結構內儲存閉包

對於閉包,我們也可以存在結構內:

use std::collections::HashMap;

struct Cacher<T>
where
    T: Fn(u32) -> u32,
{
    calculation: T,
    table: HashMap<u32, u32>,
}

可以看到泛型的結構內,我們對T進行限制,限制閉包要傳的參數型別與回傳型別,Fn是一個內建的特徵,除了Fn,還有FnMutFnOnce

然後下面是實作與簡單的測試:

impl<T> Cacher<T>
where
    T: Fn(u32) -> u32,
{
    fn new(calculation: T) -> Cacher<T> {
        Cacher {
            calculation,
            table: HashMap::new(),
        }
    }

    fn value(&mut self, arg: u32) -> u32 {
        if let Some(&value) = self.table.get(&arg) {
            value
        } else {
            let nv = (self.calculation)(arg);
            self.table.insert(arg, nv);
            nv
        }
    }
}

fn main() {
    let mut c = Cacher::new(|a| a + 6);

    let v1 = c.value(1);
    println!("{}", v1);
    let v2 = c.value(2);
    println!("{}", v2);
}

但這裡先不展開FnMutFnOnce的用法,僅介紹他們的區別,等之後遇到再來補充他們。

  1. FnOnce: 只能執行一次,取用的數值所有權會被轉移(move)
  2. Fn: 取用的值為不可變的借用
  3. FnMut: 取用的值為可變的借用

以上的說明可以對比到正常使用函式時,對參數所有權的處理。

閉包能接觸的環境

最上面有提到函式指標(以C為例)與閉包是有差別,差別在於,你可以使用與閉包定義在同個環境的變數,例如這樣:

fn main() {
    let x = 4;

    let equal_to_x = |z| z == x;

    let y = 4;

    if equal_to_x(y) {
        println!("Is same.");
    } else {
        println!("Not the same.");
    }
}

但在函式是不行的,會被compiler抱怨,說並不在同個環境內:

// 錯誤!不可執行
fn main() {
    let x = 4;

    fn equal_to_x(z: i32) -> bool {
        z == x // `x`在作用域外
    }

    let y = 4;

    if equal_to_x(y) {
        println!("Is same.");
    } else {
        println!("Not the same.");
    }
}

Reference


上一篇
[Rust] 生命週期(Lifetime)
系列文
嘗試30天學「不」會Rust18
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言